﻿using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.IO.Packaging;
using System.Net;
using System.Reflection;
using System.ServiceModel;
using System.Windows.Threading;
using MouseExtender.Logic.Common;
using MouseExtender.Logic.Entities;
using MouseExtender.Logic.Enums;
using MouseExtender.UpdateService;

namespace MouseExtender.Logic.Managers
{
    public static class UpdateManager
    {
        #region Nested types

        public class UpdateInfo
        {
            public UpdateStatus Status { get; set; }
            public string Title { get; set; }
            public string Content { get; set; }
            public Uri ReleaseUri { get; set; }
            public Version LatestVersion { get; set; }
            public Exception Error { get; set; }
            public bool IsPeriodicalCheck { get; set; }

            public UpdateInfo()
            {
            }

            public UpdateInfo(Uri downloadUri, Version version, UpdateStatus status, Exception error)
            {
                if (error != null)
                    Status = UpdateManager.UpdateStatus.None;
                else
                    Status = status;

                ReleaseUri = downloadUri;
                LatestVersion = version;
                Status = status;
                Error = error;
            }
        }

        public enum UpdateStatus
        {
            None,
            NoUpdatesAvailable,
            NewVersionAvailable,
            UpdatesDownloaded,
            UpdateComleted,
        }

        #endregion

        #region Constants and Fields

        private const string latestReleaseUrl = @"http://mouseextender.net/downloads/MouseExtender.zip";
        private const string updatesFileName = "MouseExtender.{0}.zip";
        private const string updatesFolder = "Updates";
        public const string defaultReleaseUrl = @"http://me.codeplex.com/releases/view/30128";

        private static DispatcherTimer _updateTimer = new DispatcherTimer();
        private static BackgroundWorker _checkUpdatesWorker = new BackgroundWorker();
        private static BackgroundWorker _downloadUpdatesWorker = new BackgroundWorker();
        private static BackgroundWorker _installUpdatesWorker = new BackgroundWorker();
        private static CheckUpdatesPeriod _updatesPeriod;

        #endregion

        #region Constructors

        static UpdateManager()
        {
            AutomaticUpdatePeriod = SettingsManager.CurrentSettings.CheckUpdatesPeriod.Value;
            UpdatesInfo = new UpdateInfo();
            _updateTimer.Tick += new EventHandler(CheckUpdatesPeriodicaly);
            _checkUpdatesWorker.WorkerSupportsCancellation = true;
            _checkUpdatesWorker.DoWork += new DoWorkEventHandler(OnСheckForUpdates);
            _checkUpdatesWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(OnUpdatesCheckingCompleted);
            _downloadUpdatesWorker.DoWork += new DoWorkEventHandler(OnUpdatesDownload);
            _downloadUpdatesWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(OnUpdatesDownloadCompleted);
            _installUpdatesWorker.DoWork += new DoWorkEventHandler(OnInstallUpdates);
            _installUpdatesWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(OnUpdatesInstalled);
        }

        #endregion

        #region Properties

        private static TimeSpan UpdateTimerPeriod
        {
            get
            {
                TimeSpan updatesPeriod;
                switch (_updatesPeriod)
                {
                    case CheckUpdatesPeriod.Hourly:
                        updatesPeriod = TimeSpan.FromMinutes(1);
                        break;
                    case CheckUpdatesPeriod.Dayly:
                        updatesPeriod = TimeSpan.FromMinutes(30);
                        break;
                    case CheckUpdatesPeriod.Weekly:
                    case CheckUpdatesPeriod.Monthly:
                        updatesPeriod = TimeSpan.FromHours(1);
                        break;
                    case CheckUpdatesPeriod.Never:
                    default:
                        updatesPeriod = TimeSpan.MaxValue;
                        break;
                }

                return updatesPeriod;
            }
        }

        private static TimeSpan UpdatePeriod
        {
            get
            {
                TimeSpan updatesPeriod;
                switch (_updatesPeriod)
                {
                    case CheckUpdatesPeriod.Hourly:
                        updatesPeriod = TimeSpan.FromHours(1);
                        break;
                    case CheckUpdatesPeriod.Dayly:
                        updatesPeriod = TimeSpan.FromDays(1);
                        break;
                    case CheckUpdatesPeriod.Weekly:
                        updatesPeriod = TimeSpan.FromDays(7);
                        break;
                    case CheckUpdatesPeriod.Monthly:
                        updatesPeriod = TimeSpan.FromDays(30);
                        break;
                    case CheckUpdatesPeriod.Never:
                    default:
                        updatesPeriod = TimeSpan.MaxValue;
                        break;
                }

                return updatesPeriod;
            }
        }

        private static Version NewVersion
        { get; set; }

        private static Uri DownloadUri
        {
            get
            {
                return new Uri(latestReleaseUrl);
            }
        }

        private static String UpdatesLocalPath
        {
            get
            {
                string fileName = String.Format(updatesFileName, NewVersion);
                string path = Path.Combine(updatesFolder, fileName);
                return path;
            }
        }

        /// <summary>
        /// Sets a period of automatic updates check.
        /// </summary>
        public static CheckUpdatesPeriod AutomaticUpdatePeriod
        {
            private get
            {
                return _updatesPeriod;
            }
            set
            {
                _updatesPeriod = value;

                if (value == CheckUpdatesPeriod.Never)
                {
                    _updateTimer.Stop();
                }
                else
                {
                    TimeSpan updatePeriod = UpdateTimerPeriod;
                    _updateTimer.Interval = updatePeriod;
                    _updateTimer.Start();
                }
            }
        }

        /// <summary>
        /// Gets information of current process of updates checking.
        /// </summary>
        public static UpdateInfo UpdatesInfo
        {
            get;
            private set;
        }

        #endregion

        #region Methods

        #region secondary Methods

        private static WebClient CreateWebClient()
        {
            WebClient client = new WebClient();
            client.UseDefaultCredentials = true;
            return client;
        }

        private static void CopyStream(Stream source, Stream target)
        {
            const int bufSize = 0x1000;
            byte[] buf = new byte[bufSize];
            int bytesRead = 0;
            while ((bytesRead = source.Read(buf, 0, bufSize)) > 0)
            {
                target.Write(buf, 0, bytesRead);
            }
        }

        private static void SetCurrentDirectory()
        {
            string localPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            Directory.SetCurrentDirectory(localPath);
        }

        #endregion

        #region check for updates

        public static void CheckUpdatesPeriodicaly(object state, EventArgs e)
        {
            //executed at UI thread. refactored to use asynch worker in order to remove lag during udpate check
            if (_checkUpdatesWorker.IsBusy)
                return;

            UserSettings settings = SettingsManager.CurrentSettings;
            DateTime now = DateTime.Now;

            if (state is DispatcherTimer)
            {
                DateTime lastUpdate = settings.LastUpdatesCheck ?? DateTime.MinValue;
                if (now - lastUpdate < UpdatePeriod)
                    return;
            }

            settings.LastUpdatesCheck = now;
            _checkUpdatesWorker.RunWorkerAsync(true);
        }

        /// <summary>
        /// Checks if new version of Application is available for download.
        /// </summary>
        /// <param name="latestReleaseUrl">New version package download url.</param>
        /// <returns>true if newer version is availble.</returns>
        private static bool CheckForUpdates(out Uri latestReleaseUrl, out Version latestVersion)
        {
            var updateCheck = new ApplicationInfo()
            {
                ApplicationInstanceId = SettingsManager.CurrentSettings.ApplicationInstanceId.Value,
                ApplicationVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(),
            };

            var binding = new BasicHttpBinding();
            var endPoint = new EndpointAddress(string.Format("{0}/WebServices/UpdateService.svc", Constants.MeSiteUrl));
#if DEBUG
            endPoint = new EndpointAddress("http://localhost:30016/WebServices/UpdateService.svc");
#endif
            UpdateServiceClient updateProxy = null;
            UpdateInfoEntity checkResult = null;
            try
            {
                updateProxy = new UpdateServiceClient(binding, endPoint);
                checkResult = updateProxy.IsNewVersionAvailable(updateCheck);
            }
            catch
            {
                if (updateProxy != null)
                {
                    updateProxy.Abort();
                }
                throw;
            }
            finally
            {
                if (updateProxy != null)
                {
                    updateProxy.Close();
                }
            }

            latestVersion = checkResult.LatestVersion;
            latestReleaseUrl = checkResult.LatestReleaseUrl;

            NewVersion = latestVersion;
            SettingsManager.CurrentSettings.LastUpdatesCheck = DateTime.Now;

            return checkResult.NewVersionExists;
        }

        #endregion

        #region automatical updates

        /// <summary>
        /// Starts downloading and installing of updated version.
        /// </summary>
        private static void DownloadUpdates()
        {
            if (DownloadUri != null)
            {
                DownloadLatestRelease(false);
            }
        }

        /// <summary>
        /// Downloads MouseExtender.zip from http://mouseexteder.net site and saves it in %worker_directory/Updates% folder.
        /// </summary>
        /// <param name="overwriteIfDownloaded">If false then skip download if new version was downloaded.</param>
        private static void DownloadLatestRelease(bool overwriteIfDownloaded)
        {
            SetCurrentDirectory();
            if (overwriteIfDownloaded || !File.Exists(UpdatesLocalPath))
            {
                WebClient downloadClient = CreateWebClient();
                Directory.CreateDirectory(updatesFolder);
                byte[] buffer = downloadClient.DownloadData(DownloadUri);
                File.WriteAllBytes(UpdatesLocalPath, buffer);
            }
        }

        /// <summary>
        /// Unpack MouseExtender.zip file into Updates directory.
        /// </summary>
        private static void UnpackPackage()
        {
            SetCurrentDirectory();
            if (!File.Exists(UpdatesLocalPath))
                throw new ApplicationException(String.Format("File '{0}' not found", UpdatesLocalPath));
            using (ZipPackage updates = (ZipPackage)Package.Open(UpdatesLocalPath))
            {
                foreach (var part in updates.GetParts())
                {
                    string filepath = updatesFolder + part.Uri.ToString();
                    Directory.CreateDirectory(Path.GetDirectoryName(filepath));

                    using (FileStream fileStream = new FileStream(filepath, FileMode.Create))
                    {
                        CopyStream(part.GetStream(), fileStream);
                    }
                }
            }
            File.Delete(UpdatesLocalPath);
        }

        /// <summary>
        /// Launches cmd.exe and pathes commands in order to update mouseextender.exe and resources files and cleanup.
        /// </summary>
        private static void LaunchOverwriteCommands()
        {
            SetCurrentDirectory();
            string commands = @"TSKILL mouseextender &  xcopy updates\* /E /Y & rd updates /S /Q & rd _rels /S /Q & mouseextender.exe /Updated";
            ProcessStartInfo processInfo = new ProcessStartInfo("cmd.exe", "/C " + commands);
            processInfo.CreateNoWindow = true;
            processInfo.WindowStyle = ProcessWindowStyle.Hidden;
            processInfo.UseShellExecute = true;
            Process.Start(processInfo);
        }

        #endregion

        #region steps and notifications

        /// <summary>
        /// Raised related event taking into account UpdateInfo
        /// </summary>
        /// <param name="args">Check result data</param>
        private static void RaiseUpdateStatusChanged(UpdateInfo info)
        {
            UpdatesInfo = info;
            UpdateStatusChanged(info);
            if (info.Status == UpdateStatus.NewVersionAvailable)
            {
                DownloadUpdatesAsync();
            }
        }

        private static void OnСheckForUpdates(object sender, DoWorkEventArgs e)
        {
            UpdateInfo info = new UpdateInfo();
            e.Result = info;

            Version version = null;
            Uri uri = null;
            bool isNew = CheckForUpdates(out uri, out version);

            info.LatestVersion = version;
            info.ReleaseUri = uri;
            info.Status = isNew ? UpdateStatus.NewVersionAvailable : UpdateStatus.NoUpdatesAvailable;
            info.IsPeriodicalCheck = e.Argument as bool? ?? false;
        }

        private static void OnUpdatesCheckingCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            BackgroundWorker worker = sender as BackgroundWorker;
            if (worker.CancellationPending)
                return;

            UpdateInfo info = new UpdateInfo();
            if (e.Error != null)
            {
                Logger.HandleNonCriticalError(e.Error);
                info.Error = e.Error;
            }
            else
            {
                info = e.Result as UpdateInfo;
            }

            bool nonPeriodcalCheckOrNewVersion = !info.IsPeriodicalCheck || info.Status >= UpdateStatus.NewVersionAvailable;
            bool errorDuringNonPeriodicalCheck = !info.IsPeriodicalCheck && info.Error != null;

            if (nonPeriodcalCheckOrNewVersion || errorDuringNonPeriodicalCheck)
            {
                RaiseUpdateStatusChanged(info);
            }
            else
            {
                UpdatesInfo.Status = UpdateStatus.None;
            }
        }

        private static void OnUpdatesDownload(object sender, DoWorkEventArgs e)
        {
            DownloadUpdates();
        }

        private static void OnUpdatesDownloadCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (e.Error != null)
            {
                Logger.HandleNonCriticalError(e.Error);
                UpdatesInfo.Error = e.Error;
                UpdatesInfo.Status = UpdateStatus.None;
            }
            else
            {
                UpdatesInfo.Status = UpdateStatus.UpdatesDownloaded;
            }

            RaiseUpdateStatusChanged(UpdatesInfo);
        }

        private static void OnInstallUpdates(object sender, DoWorkEventArgs e)
        {
            UnpackPackage();
            LaunchOverwriteCommands();
        }

        private static void OnUpdatesInstalled(object sender, RunWorkerCompletedEventArgs e)
        {
            if (e.Error != null)
            {
                Logger.HandleNonCriticalError(e.Error);
                UpdatesInfo.Error = e.Error;
                UpdatesInfo.Status = UpdateStatus.None;
            }
            else
            {
                UpdatesInfo.Status = UpdateStatus.UpdateComleted;
            }

            RaiseUpdateStatusChanged(UpdatesInfo);
        }

        #endregion

        /// <summary>
        /// Checks if new version of Application is available for download asyncronously.
        /// </summary>
        public static void CheckForUpdatesAsync()
        {
            if (!_checkUpdatesWorker.IsBusy)
                _checkUpdatesWorker.RunWorkerAsync(false);
        }

        /// <summary>
        /// Downloads updates asyncronously.
        /// </summary>
        public static void DownloadUpdatesAsync()
        {
            if (!_downloadUpdatesWorker.IsBusy)
                _downloadUpdatesWorker.RunWorkerAsync();
        }

        /// <summary>
        /// Installes downloaded updates asyncronously.
        /// </summary>
        public static void InstallUpdatesAsync()
        {
            if (!_installUpdatesWorker.IsBusy)
                _installUpdatesWorker.RunWorkerAsync();
        }

        /// <summary>
        /// Rise update notification after update completed.
        /// </summary>
        public static void Updated()
        {
            UpdateInfo info = new UpdateInfo(null, Assembly.GetExecutingAssembly().GetName().Version, UpdateStatus.UpdateComleted, null);
            RaiseUpdateStatusChanged(info);
            UpdatesInfo.Status = UpdateStatus.None;
        }

        #endregion

        #region Events

        public static event Action<UpdateInfo> UpdateStatusChanged = (i) => { };

        #endregion
    }
}
